/* Detect-zoom
* -----------
* Cross Browser Zoom and Pixel Ratio Detector
* Version 1.0.4 | Apr 1 2013
* dual-licensed under the WTFPL and MIT license
* Maintained by https://github/tombigel
* Original developer https://github.com/yonran
*/
//AMD and CommonJS initialization copied from https://github.com/zohararad/audio5js
(function (root, ns, factory) {
"use strict";
if (typeof (module) !== 'undefined' && module.exports) { // CommonJS
module.exports = factory(ns, root);
} else if (typeof (define) === 'function' && define.amd) { // AMD
define("factory", function () {
return factory(ns, root);
});
} else {
root[ns] = factory(ns, root);
}
}(window, 'detectZoom', function () {
/**
* Use devicePixelRatio if supported by the browser
* @return {Number}
* @private
*/
var devicePixelRatio = function () {
return window.devicePixelRatio || 1;
};
/**
* Fallback function to set default values
* @return {Object}
* @private
*/
var fallback = function () {
return {
zoom: 1,
devicePxPerCssPx: 1
};
};
/**
* IE 8 and 9: no trick needed!
* TODO: Test on IE10 and Windows 8 RT
* @return {Object}
* @private
**/
var ie8 = function () {
var zoom = Math.round((screen.deviceXDPI / screen.logicalXDPI) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * devicePixelRatio()
};
};
/**
* For IE10 we need to change our technique again...
* thanks https://github.com/stefanvanburen
* @return {Object}
* @private
*/
var ie10 = function () {
var zoom = Math.round((document.documentElement.offsetHeight / window.innerHeight) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * devicePixelRatio()
};
};
/**
* Mobile WebKit
* the trick: window.innerWIdth is in CSS pixels, while
* screen.width and screen.height are in system pixels.
* And there are no scrollbars to mess up the measurement.
* @return {Object}
* @private
*/
var webkitMobile = function () {
var deviceWidth = (Math.abs(window.orientation) == 90) ? screen.height : screen.width;
var zoom = deviceWidth / window.innerWidth;
return {
zoom: zoom,
devicePxPerCssPx: zoom * devicePixelRatio()
};
};
/**
* Desktop Webkit
* the trick: an element's clientHeight is in CSS pixels, while you can
* set its line-height in system pixels using font-size and
* -webkit-text-size-adjust:none.
* device-pixel-ratio: http://www.webkit.org/blog/55/high-dpi-web-sites/
*
* Previous trick (used before http://trac.webkit.org/changeset/100847):
* documentElement.scrollWidth is in CSS pixels, while
* document.width was in system pixels. Note that this is the
* layout width of the document, which is slightly different from viewport
* because document width does not include scrollbars and might be wider
* due to big elements.
* @return {Object}
* @private
*/
var webkit = function () {
var important = function (str) {
return str.replace(/;/g, " !important;");
};
var div = document.createElement('div');
div.innerHTML = "1 2 3 4 5 6 7 8 9 0";
div.setAttribute('style', important('font: 100px/1em sans-serif; -webkit-text-size-adjust: none; text-size-adjust: none; height: auto; width: 1em; padding: 0; overflow: visible;'));
// The container exists so that the div will be laid out in its own flow
// while not impacting the layout, viewport size, or display of the
// webpage as a whole.
// Add !important and relevant CSS rule resets
// so that other rules cannot affect the results.
var container = document.createElement('div');
container.setAttribute('style', important('width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;'));
container.appendChild(div);
document.body.appendChild(container);
var zoom = 1000 / div.clientHeight;
zoom = Math.round(zoom * 100) / 100;
document.body.removeChild(container);
return{
zoom: zoom,
devicePxPerCssPx: zoom * devicePixelRatio()
};
};
/**
* no real trick; device-pixel-ratio is the ratio of device dpi / css dpi.
* (Note that this is a different interpretation than Webkit's device
* pixel ratio, which is the ratio device dpi / system dpi).
*
* Also, for Mozilla, there is no difference between the zoom factor and the device ratio.
*
* @return {Object}
* @private
*/
var firefox4 = function () {
var zoom = mediaQueryBinarySearch('min--moz-device-pixel-ratio', '', 0, 10, 20, 0.0001);
zoom = Math.round(zoom * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom
};
};
/**
* Firefox 18.x
* Mozilla added support for devicePixelRatio to Firefox 18,
* but it is affected by the zoom level, so, like in older
* Firefox we can't tell if we are in zoom mode or in a device
* with a different pixel ratio
* @return {Object}
* @private
*/
var firefox18 = function () {
return {
zoom: firefox4().zoom,
devicePxPerCssPx: devicePixelRatio()
};
};
/**
* works starting Opera 11.11
* the trick: outerWidth is the viewport width including scrollbars in
* system px, while innerWidth is the viewport width including scrollbars
* in CSS px
* @return {Object}
* @private
*/
var opera11 = function () {
var zoom = window.top.outerWidth / window.top.innerWidth;
zoom = Math.round(zoom * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * devicePixelRatio()
};
};
/**
* Use a binary search through media queries to find zoom level in Firefox
* @param property
* @param unit
* @param a
* @param b
* @param maxIter
* @param epsilon
* @return {Number}
*/
var mediaQueryBinarySearch = function (property, unit, a, b, maxIter, epsilon) {
var matchMedia;
var head, style, div;
if (window.matchMedia) {
matchMedia = window.matchMedia;
} else {
head = document.getElementsByTagName('head')[0];
style = document.createElement('style');
head.appendChild(style);
div = document.createElement('div');
div.className = 'mediaQueryBinarySearch';
div.style.display = 'none';
document.body.appendChild(div);
matchMedia = function (query) {
style.sheet.insertRule('@media ' + query + '{.mediaQueryBinarySearch ' + '{text-decoration: underline} }', 0);
var matched = getComputedStyle(div, null).textDecoration == 'underline';
style.sheet.deleteRule(0);
return {matches: matched};
};
}
var ratio = binarySearch(a, b, maxIter);
if (div) {
head.removeChild(style);
document.body.removeChild(div);
}
return ratio;
function binarySearch(a, b, maxIter) {
var mid = (a + b) / 2;
if (maxIter <= 0 || b - a < epsilon) {
return mid;
}
var query = "(" + property + ":" + mid + unit + ")";
if (matchMedia(query).matches) {
return binarySearch(mid, b, maxIter - 1);
} else {
return binarySearch(a, mid, maxIter - 1);
}
}
};
/**
* Generate detection function
* @private
*/
var detectFunction = (function () {
var func = fallback;
//IE8+
if (!isNaN(screen.logicalXDPI) && !isNaN(screen.systemXDPI)) {
func = ie8;
}
// IE10+ / Touch
else if (window.navigator.msMaxTouchPoints) {
func = ie10;
}
//Mobile Webkit
else if ('orientation' in window && typeof document.body.style.webkitMarquee === 'string') {
func = webkitMobile;
}
//WebKit
else if (typeof document.body.style.webkitMarquee === 'string') {
func = webkit;
}
//Opera
else if (navigator.userAgent.indexOf('Opera') >= 0) {
func = opera11;
}
//Last one is Firefox
//FF 18.x
else if (window.devicePixelRatio) {
func = firefox18;
}
//FF 4.0 - 17.x
else if (firefox4().zoom > 0.001) {
func = firefox4;
}
return func;
}());
return ({
/**
* Ratios.zoom shorthand
* @return {Number} Zoom level
*/
zoom: function () {
return detectFunction().zoom;
},
/**
* Ratios.devicePxPerCssPx shorthand
* @return {Number} devicePxPerCssPx level
*/
device: function () {
return detectFunction().devicePxPerCssPx;
}
});
}));
var wpcom_img_zoomer = {
zoomed: false,
timer: null,
interval: 1000, // zoom polling interval in millisecond
// Should we apply width/height attributes to control the image size?
imgNeedsSizeAtts: function( img ) {
// Do not overwrite existing width/height attributes.
if ( img.getAttribute('width') !== null || img.getAttribute('height') !== null )
return false;
// Do not apply the attributes if the image is already constrained by a parent element.
if ( img.width < img.naturalWidth || img.height < img.naturalHeight )
return false;
return true;
},
init: function() {
var t = this;
try{
t.zoomImages();
t.timer = setInterval( function() { t.zoomImages(); }, t.interval );
}
catch(e){
}
},
stop: function() {
if ( this.timer )
clearInterval( this.timer );
},
getScale: function() {
var scale = detectZoom.device();
// Round up to 1.5 or the next integer below the cap.
if ( scale <= 1.0 ) scale = 1.0;
else if ( scale <= 1.5 ) scale = 1.5;
else if ( scale <= 2.0 ) scale = 2.0;
else if ( scale <= 3.0 ) scale = 3.0;
else if ( scale <= 4.0 ) scale = 4.0;
else scale = 5.0;
return scale;
},
shouldZoom: function( scale ) {
var t = this;
// Do not operate on hidden frames.
if ( "innerWidth" in window && !window.innerWidth )
return false;
// Don't do anything until scale > 1
if ( scale == 1.0 && t.zoomed == false )
return false;
return true;
},
zoomImages: function() {
var t = this;
var scale = t.getScale();
if ( ! t.shouldZoom( scale ) ){
return;
}
t.zoomed = true;
// Loop through all the elements on the page.
var imgs = document.getElementsByTagName("img");
for ( var i = 0; i < imgs.length; i++ ) {
// Wait for original images to load
if ( "complete" in imgs[i] && ! imgs[i].complete )
continue;
// Skip images that don't need processing.
var imgScale = imgs[i].getAttribute("scale");
if ( imgScale == scale || imgScale == "0" )
continue;
// Skip images that have already failed at this scale
var scaleFail = imgs[i].getAttribute("scale-fail");
if ( scaleFail && scaleFail <= scale )
continue;
// Skip images that have no dimensions yet.
if ( ! ( imgs[i].width && imgs[i].height ) )
continue;
if ( t.scaleImage( imgs[i], scale ) ) {
// Mark the img as having been processed at this scale.
imgs[i].setAttribute("scale", scale);
}
else {
// Set the flag to skip this image.
imgs[i].setAttribute("scale", "0");
}
}
},
scaleImage: function( img, scale ) {
var t = this;
var newSrc = img.src;
// Skip slideshow images
if ( img.parentNode.className.match(/slideshow-slide/) )
return false;
// Scale gravatars that have ?s= or ?size=
if ( img.src.match( /^https?:\/\/([^\/]*\.)?gravatar\.com\/.+[?&](s|size)=/ ) ) {
newSrc = img.src.replace( /([?&](s|size)=)(\d+)/, function( $0, $1, $2, $3 ) {
// Stash the original size
var originalAtt = "originals",
originalSize = img.getAttribute(originalAtt);
if ( originalSize === null ) {
originalSize = $3;
img.setAttribute(originalAtt, originalSize);
if ( t.imgNeedsSizeAtts( img ) ) {
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
}
}
// Get the width/height of the image in CSS pixels
var size = img.clientWidth;
// Convert CSS pixels to device pixels
var targetSize = Math.ceil(img.clientWidth * scale);
// Don't go smaller than the original size
targetSize = Math.max( targetSize, originalSize );
// Don't go larger than the service supports
targetSize = Math.min( targetSize, 512 );
return $1 + targetSize;
});
}
// Scale resize queries (*.files.wordpress.com) that have ?w= or ?h=
else if ( img.src.match( /^https?:\/\/([^\/]+)\.files\.wordpress\.com\/.+[?&][wh]=/ ) ) {
if ( img.src.match( /[?&]crop/ ) )
return false;
var changedAttrs = {};
var matches = img.src.match( /([?&]([wh])=)(\d+)/g );
for ( var i = 0; i < matches.length; i++ ) {
var lr = matches[i].split( '=' );
var thisAttr = lr[0].replace(/[?&]/g, '' );
var thisVal = lr[1];
// Stash the original size
var originalAtt = 'original' + thisAttr, originalSize = img.getAttribute( originalAtt );
if ( originalSize === null ) {
originalSize = thisVal;
img.setAttribute(originalAtt, originalSize);
if ( t.imgNeedsSizeAtts( img ) ) {
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
}
}
// Get the width/height of the image in CSS pixels
var size = thisAttr == 'w' ? img.clientWidth : img.clientHeight;
var naturalSize = ( thisAttr == 'w' ? img.naturalWidth : img.naturalHeight );
// Convert CSS pixels to device pixels
var targetSize = Math.ceil(size * scale);
// Don't go smaller than the original size
targetSize = Math.max( targetSize, originalSize );
// Don't go bigger unless the current one is actually lacking
if ( scale > img.getAttribute("scale") && targetSize <= naturalSize )
targetSize = thisVal;
// Don't try to go bigger if the image is already smaller than was requested
if ( naturalSize < thisVal )
targetSize = thisVal;
if ( targetSize != thisVal )
changedAttrs[ thisAttr ] = targetSize;
}
var w = changedAttrs.w || false;
var h = changedAttrs.h || false;
if ( w ) {
newSrc = img.src.replace(/([?&])w=\d+/g, function( $0, $1 ) {
return $1 + 'w=' + w;
});
}
if ( h ) {
newSrc = newSrc.replace(/([?&])h=\d+/g, function( $0, $1 ) {
return $1 + 'h=' + h;
});
}
}
// Scale mshots that have width
else if ( img.src.match(/^https?:\/\/([^\/]+\.)*(wordpress|wp)\.com\/mshots\/.+[?&]w=\d+/) ) {
newSrc = img.src.replace( /([?&]w=)(\d+)/, function($0, $1, $2) {
// Stash the original size
var originalAtt = 'originalw', originalSize = img.getAttribute(originalAtt);
if ( originalSize === null ) {
originalSize = $2;
img.setAttribute(originalAtt, originalSize);
if ( t.imgNeedsSizeAtts( img ) ) {
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
}
}
// Get the width of the image in CSS pixels
var size = img.clientWidth;
// Convert CSS pixels to device pixels
var targetSize = Math.ceil(size * scale);
// Don't go smaller than the original size
targetSize = Math.max( targetSize, originalSize );
// Don't go bigger unless the current one is actually lacking
if ( scale > img.getAttribute("scale") && targetSize <= img.naturalWidth )
targetSize = $2;
if ( $2 != targetSize )
return $1 + targetSize;
return $0;
});
}
// Scale simple imgpress queries (s0.wp.com) that only specify w/h/fit
else if ( img.src.match(/^https?:\/\/([^\/.]+\.)*(wp|wordpress)\.com\/imgpress\?(.+)/) ) {
var imgpressSafeFunctions = ["zoom", "url", "h", "w", "fit", "filter", "brightness", "contrast", "colorize", "smooth", "unsharpmask"];
// Search the query string for unsupported functions.
var qs = RegExp.$3.split('&');
for ( var q in qs ) {
q = qs[q].split('=')[0];
if ( imgpressSafeFunctions.indexOf(q) == -1 ) {
return false;
}
}
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
// Compute new src
if ( scale == 1 )
newSrc = img.src.replace(/\?(zoom=[^&]+&)?/, '?');
else
newSrc = img.src.replace(/\?(zoom=[^&]+&)?/, '?zoom=' + scale + '&');
}
// Scale LaTeX images or Photon queries (i#.wp.com)
else if (
img.src.match(/^https?:\/\/([^\/.]+\.)*(wp|wordpress)\.com\/latex\.php\?(latex|zoom)=(.+)/) ||
img.src.match(/^https?:\/\/i[\d]{1}\.wp\.com\/(.+)/)
) {
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
// Compute new src
if ( scale == 1 )
newSrc = img.src.replace(/\?(zoom=[^&]+&)?/, '?');
else
newSrc = img.src.replace(/\?(zoom=[^&]+&)?/, '?zoom=' + scale + '&');
}
// Scale static assets that have a name matching *-1x.png or *@1x.png
else if ( img.src.match(/^https?:\/\/[^\/]+\/.*[-@]([12])x\.(gif|jpeg|jpg|png)(\?|$)/) ) {
// Fix width and height attributes to rendered dimensions.
img.width = img.width;
img.height = img.height;
var currentSize = RegExp.$1, newSize = currentSize;
if ( scale <= 1 )
newSize = 1;
else
newSize = 2;
if ( currentSize != newSize )
newSrc = img.src.replace(/([-@])[12]x\.(gif|jpeg|jpg|png)(\?|$)/, '$1'+newSize+'x.$2$3');
}
else {
return false;
}
// Don't set img.src unless it has changed. This avoids unnecessary reloads.
if ( newSrc != img.src ) {
// Store the original img.src
var prevSrc, origSrc = img.getAttribute("src-orig");
if ( !origSrc ) {
origSrc = img.src;
img.setAttribute("src-orig", origSrc);
}
// In case of error, revert img.src
prevSrc = img.src;
img.onerror = function(){
img.src = prevSrc;
if ( img.getAttribute("scale-fail") < scale )
img.setAttribute("scale-fail", scale);
img.onerror = null;
};
// Finally load the new image
img.src = newSrc;
}
return true;
}
};
wpcom_img_zoomer.init();
;
/*!
* jQuery Cookie Plugin v1.3.1
* https://github.com/carhartl/jquery-cookie
*
* Copyright 2013 Klaus Hartl
* Released under the MIT license
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as anonymous module.
define(['jquery'], factory);
} else {
// Browser globals.
factory(jQuery);
}
}(function ($) {
var pluses = /\+/g;
function raw(s) {
return s;
}
function decoded(s) {
return decodeURIComponent(s.replace(pluses, ' '));
}
function converted(s) {
if (s.indexOf('"') === 0) {
// This is a quoted cookie as according to RFC2068, unescape
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}
try {
return config.json ? JSON.parse(s) : s;
} catch(er) {}
}
var config = $.cookie = function (key, value, options) {
// write
if (value !== undefined) {
options = $.extend({}, config.defaults, options);
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setDate(t.getDate() + days);
}
value = config.json ? JSON.stringify(value) : String(value);
return (document.cookie = [
config.raw ? key : encodeURIComponent(key),
'=',
config.raw ? value : encodeURIComponent(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// read
var decode = config.raw ? raw : decoded;
var cookies = document.cookie.split('; ');
var result = key ? undefined : {};
for (var i = 0, l = cookies.length; i < l; i++) {
var parts = cookies[i].split('=');
var name = decode(parts.shift());
var cookie = decode(parts.join('='));
if (key && key === name) {
result = converted(cookie);
break;
}
if (!key) {
result[name] = converted(cookie);
}
}
return result;
};
config.defaults = {};
$.removeCookie = function (key, options) {
if ($.cookie(key) !== undefined) {
// Must not alter options, thus extending a fresh object...
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
return true;
}
return false;
};
}));
;
/**
* jQuery qToggle Plugin
* Allows you to toggle (hide/show) DOM elements just by applying data attributes to a controlling HTML element.
* Supports many jQuery animations like slideToggle, fadeToggle, etc., and also support animation settings such as duration, easing, and callbacks.
* @developer Nathan Letsinger
* @since 1.0
* @requires jQuery 1.7.2
*
* Copyright 2012 Nathan Letsinger
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**/
(function($){
jQuery.fn.qToggle = function(options){
// settings with defaults
var settings = jQuery.extend({
'data' : {}, // object - passed into event (reserved for later use)
'effect' : 'toggle', // string - the animation effect on target
'events' : 'click.qToggle', // string - the event on control (namespaced)
'context' : '[data-qtoggle-selector]', // string - a selector that defines a control
'selector' : 'qtoggle-selector', // string - the name of the data that holds selector string of target
'targets' : 'prev', // string - the default target if none is specified in settings.selector
'innerHTML' : '', // string - html to insert into control's text node. Empty string for no change
'eventArgs' : { // object - arguments passed to the effect function
'duration' : null, // int|string - duration of animation in miliseconds or keywords 'fast','slow',etc.
'easing' :'linear', // string - the animation effect, 'linear' or 'swing' are the only options in native jQuery but other plugins may provide other options
'callback' : null // string|function - the function to call when animation is complete
}
}, options);
// listen for events on the selector
this.on( settings.events, settings.context, settings.data, function(event){
// explicitly stop default behavior of events on this control
event.preventDefault();
// explicitly stop bubbling
event.stopPropagation();
/* @var object - a whitelist of available effects and DOM transversal keywords */
var defaults = {
'effects' :[ 'toggle', 'slideToggle', 'fadeToggle', 'hide' , 'show' , 'fadeOut', 'fadeIn', 'slideUp' , 'slideDown' ],
'transversals' : [ 'prev' ,'next', 'parent', 'siblings', 'nextAll', 'prevAll' ,'children' ]
};
/* @var object - the jQuery selection of the toggle control element */
var control = jQuery(this);
// @todo throw an error if given effect is not in our whitelist
/* @var string - the name of the effect function desired, 'toggle' by default */
var effect = control.data('qtoggle-effect') || settings.effect;
effect = (jQuery.inArray( effect, defaults.effects ) === -1) ? defaults.effects[0] : effect;
/* @var string - the selector string for our targeted DOM element to be effected */
var targets = control.data( settings.selector ) || settings.targets;
/* @var string - the new value of the control's innerHTML, if any */
var innerHTML = ( control.data('qtoggle-text') || settings.innerHTML );
/* @var string|int - the timing for the animation */
var duration = control.data('qtoggle-duration') || settings.eventArgs.duration;
/* @var string - the timing for the animation */
var easing = control.data('qtoggle-easing') || settings.eventArgs.easing;
/* @var function|null - the callback function, if any */
var callback = control.data('qtoggle-callback') || settings.eventArgs.callback;
callback = (typeof window[callback] === 'function' ) ? window[callback] : null
// possibly update the innerHTML of the control element
if( innerHTML ) {
control.data( 'qtoggle-text', control.html() );
control.html( innerHTML );
}
// apply the desired effect on the DOM targets:
// first we check if we are using a DOM transversal keyword like 'next' or 'prev'
if( jQuery.inArray(targets, defaults.transversals) !== -1 )
control[targets]()[effect]( duration, easing, callback );
// then we check if we should effect the control itself
else if( targets === 'this' || targets === 'self' )
control[effect]( duration, easing, callback );
// otherwise we select the target using css selectors
else
jQuery(targets)[effect]( duration, easing, callback );
return false; // stop propogations and default events
});// end this.on()
// I'm a chainable function
return this;
}})( jQuery );;
/**
* Universal Grist Javascript
*
* Mostly used for getting and setting user-specific data
*
* Requires jQuery, jquery.cookie
* Copyright (c) 2009-2112 Grist (grist.org)
*/
var GRIST = GRIST || {};
jQuery(function(){
// Get what we know about the reader from localStorage
GRIST.READER.init();
// Track subscriptions
GRIST.SUBSCRIPTION_TRACKING.init();
GRIST.NEWSLETTER_ARRIVAL_CAPTURE.init();
// Track visits
GRIST.VISIT.init();
GRIST.VISIT.MONTHLY_COUNTER.init();
//GRIST.VISIT.PAGEVIEWS.init();
// Track donations
GRIST.DONATION_TRACKING.init();
// Track readers who reach the end of articles
GRIST.DEPTH_TRACKING.init();
// Track discovery mechanism usage
GRIST.DISCOVERY_TRACKING.init();
});
/**
* GRIST.READER is an object that represents a reader.
* you can access:
*
* email
* zip - note that zip is no longer collected
* subs
* visits
* is_subscriber
* is_subscriber_facebook
* is_subscriber_email
* is_subscriber_twittter
* is_subscriber( sub_name )
* is_donor
*
* @ToDo: visit_this_month count
* @ToDo: Last visit
* @ToDo: Last visit this month
*/
GRIST.READER = {
init: function(){
if( ! GRIST.STORAGE.is_enabled )
return false;
// Populate the object from local storage
this.update();
// Listen for changes
jQuery('body').on( 'grist_storage_change', jQuery.proxy( this.update, this ) );
},
/**
* Pulls information about the reader from local storage
*/
update: function(){
this.email = GRIST.STORAGE.get('email');
this.subs = GRIST.STORAGE.get('subs');
this.zip = GRIST.STORAGE.get('zip');
this.visits = GRIST.STORAGE.get('visits');
this.is_subscriber = this.is_subscribed_to();
this.is_subscriber_facebook = this.is_subscribed_to('facebook');
this.is_subscriber_email = this.is_subscribed_to('email');
this.is_subscriber_twitter = this.is_subscribed_to('twitter');
this.is_donor = (function(){
var donations = GRIST.STORAGE.get('donations');
return ( jQuery.isArray( donations ) && donations.length > 0 );
})();
},
/**
* Is the user a subscriber? If a specific subscription name is provided
* we check for that string in the array of subscriptions
* @param string sub_name name of the subscription to search for
* @return bool
*/
is_subscribed_to: function( sub_name ){
if( ! jQuery.isArray( this.subs ) )
return false;
if( typeof sub_name === 'undefined' )
return this.subs.length > 0;
var is_subscribed = false;
jQuery.each( this.subs, function(i, sub){
is_subscribed = sub.indexOf( sub_name ) !== -1;
// break out of the loop if we found a subscription
return ! is_subscribed;
});
return is_subscribed;
},
/**
* Marks the user as having a series of subscriptions ('email-daily', 'fb', 'twitter', etc)
* @param array list of subscription names
*/
add_subscriptions: function( new_subscriptions ){
var current_subscriptions = this.subs || [],
unique_subscriptions = [];
if( ! jQuery.isArray( new_subscriptions ) || ! jQuery.isArray( current_subscriptions ) )
return;
// combine newsletter lists
jQuery.merge( current_subscriptions, new_subscriptions );
// remove duplicates
jQuery.each( current_subscriptions, function( i, el ){
if( jQuery.inArray( el, unique_subscriptions ) === -1) unique_subscriptions.push( el );
});
GRIST.STORAGE.set( 'subs', unique_subscriptions );
this.subs = unique_subscriptions;
}
}; // GRIST.READER
/**
* Methods to get and set different values in local storage
*/
GRIST.STORAGE = {
key_prefix: 'grist_',
/**
* Gets a value from storage
* @return object
*/
get: function( key ){
if( typeof key === 'undefined')
return false;
key = this.key_prefix + key;
try{
return JSON.parse( localStorage.getItem( key ) );
} catch(e){
return false;
}
},
/**
* Sets a value in storage
* @param string key
* @param mixed val
*/
set: function( key, val ){
if( typeof key === 'undefined' || typeof val === 'undefined' )
return false;
key = this.key_prefix + key;
try{
localStorage.setItem( key, JSON.stringify( val ) );
jQuery('body').trigger( 'grist_storage_change' );
return true;
} catch(e){
return false;
}
},
/**
* Adds values to an already existing array
*
* @param string key The key of the array stored in localStorage
* @param array new_vals The values to be added to the array
*/
add_to_array: function( key, new_vals ){
if( typeof key !== 'string' || ! jQuery.isArray( new_vals ) || new_vals.length < 1 )
return ;
// get current array
var current_array = this.get( key ) || [];
// combine arrays
var combined_array = jQuery.merge( current_array, new_vals );
this.set( key, combined_array );
},
/**
* Increments a counter stored in local storage
* @param string key Key of the counter in localStorage
*/
increment_counter: function( key ){
if( typeof key !== 'string' )
return;
var counter = parseInt( this.get( key ), 10 );
if( isNaN( counter ) )
counter = 0;
this.set( key, counter + 1 );
},
/**
* Does this browser support localStorage?
* @see - http://diveintohtml5.info/detect.html#storage
*/
is_enabled: (function(){
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch(e){
return false;
}
})()
}; // GRIST.STORAGE
/**
* Grist Subscription Tracking
* @requires jQuery, 'grist-universal-js', jquery.cookie
*
* Listens for subscriptions to email, facebook, or twitter
*
* When a user subscribes:
*
* Fire KM events:
* - subscribe
* -- subscription_type: email, facebook, twitter
* -- subscription_location: flyout, sidebar
* - subscribe-[type]
* - subscribe-location-[location]
*
* Assign KM properties:
* - subscriber
* - subscriber-[type]
* - subscriber-[specific type] ex. subscriber-daily
*
* Fire GA event
* - Category: Subscriptions
* - Action: URL subscribed from
* - Label: subscription_type
*
* Extra GA events (if data available):
* - subscribed_in_location
*
* ID user in KM with their email
*
* Store user information in localStorage
* - add to subscriptons array [ email-daily, fb, email-food, twitter ]
* - email
*
*/
GRIST.SUBSCRIPTION_TRACKING = {
/**
* Listens for subscribes and old cookies
*/
init: function(){
jQuery('body').on('grist_subscribe', jQuery.proxy( this.record_sub_and_identify, this) );
// Convert old cookies to new local storage
this.legacy_cookie_conversion();
},
/**
*
* Records subscription in analytics and
* applies properties to the user
*
* @param event e
* @param object sub_data The subscription data. ex. { sub_type: 'email', subs: ['daily'] }
*
* @see GRIST.EMAIL_SUB.get_subscription_data() for exact format of email subscription data
*/
record_sub_and_identify: function(e, sub_data){
if( typeof sub_data !== 'object' || ! sub_data.hasOwnProperty('sub_type') || ! sub_data.hasOwnProperty('subs') )
return;
this.record_subscription( sub_data );
this.mark_as_subscriber( sub_data );
},
/**
* Records subscription in analytics
* @param object sub_data The subscription data. ex. { sub_type: 'email', subs: ['daily'] }
*/
record_subscription: function( sub_data ){
// Record subscription in GA
// @ToDo: review category, label, action
_gaq.push(['_trackEvent', 'subscriptions', document.URL, sub_data.sub_type, 0, true]);
// Record subscription in KM
var km_event_props = {};
// Record subscribe location if there is one
if( sub_data.hasOwnProperty( 'sub_location' ) && sub_data.sub_location ){
km_event_props.subscription_location = sub_data.sub_location;
_kmq.push(['record', 'subscribe-location-' + sub_data.sub_location]);
_gaq.push(['_trackEvent', 'subscription_meta', 'subscribed_in_location', sub_data.sub_location, 0, true]);
}
_kmq.push(['record', 'subscribe', km_event_props]);
_kmq.push(['record', 'subscribe-' + sub_data.sub_type]);
if( sub_data.sub_type === 'email' ){
// Fire a subscribe event for each specific subscription
jQuery.each( sub_data.subs, function( i, val){
_kmq.push(['record', 'subscribe-' + val]);
});
}
},
/**
* Marks the user as a subscriber in local storage and kissmetrics
* @param object sub_data The subscription data. ex. { sub_type: 'email', subs: ['daily'] }
*/
mark_as_subscriber: function( sub_data ){
var km_user_props = { subscriber: 1 },
subscriber_label_prefix = 'subscriber-';
// Mark the user as subscriber of this type. ie. subscriber-facebook, subscriber-email
km_user_props[ subscriber_label_prefix + sub_data.sub_type ] = 1;
// If user subscribed to email, assign KM props and prefix subscriptions with 'email'
if( sub_data.sub_type === 'email' ){
// Add each subscription to the KM user as a property
jQuery.each( sub_data.subs, function( i, val){
km_user_props[ subscriber_label_prefix + val ] = 1;
});
// Prefix subs with the subscription type, i.e. 'email-daily' instead of 'daily'
sub_data.subs = this.prefix_subs_array( sub_data.sub_type, sub_data.subs );
} // if email subscription
// Push the properties to KM
_kmq.push(['set', km_user_props ]);
// Add subscriptions to persistent storage
GRIST.READER.add_subscriptions( sub_data.subs );
// Record email
if( sub_data.hasOwnProperty('sub_email') ){
_kmq.push(['identify', sub_data.sub_email ]);
GRIST.STORAGE.set( 'email', sub_data.sub_email );
}
},
/**
* Fires a subscription event that other plugins can listen for and act on
*
* The properties 'sub_type' and 'subs' are required
*
* @param object sub_data ex. { sub_type: 'email', subs: ['daily', 'food'] }
*/
announce_subscribe: function( sub_data ){
if( typeof sub_data !== 'object' || ! sub_data.hasOwnProperty('sub_type') || ! sub_data.hasOwnProperty('subs') )
return;
jQuery('body').trigger( 'grist_subscribe', sub_data );
},
/**
* Adds a prefix to the subscriptions
* @param string prefix [description]
* @param array subs ex. ['daily', 'food']
* @return array Prefixed subscription list ex. ['email-daily', 'email-food']
*/
prefix_subs_array: function( prefix, subs ){
jQuery.each( subs, function( i, val ){
subs[i] = prefix + '-' + val;
});
return subs;
},
/**
* Looks for the old 'grist_subscriber' cookie
* and write the appropriate values to localStorage before
* deleting it
*/
legacy_cookie_conversion: function(){
try{
legacy_cookie = jQuery.cookie( 'grist_subscriber' );
if( ! legacy_cookie )
return;
// Parse old cookie
var sub_data = legacy_cookie.split('|'),
email = sub_data[0],
lists = sub_data[1];
// Break up lists into an array
lists = lists.split(',');
// Prefix lists with subscription type
this.prefix_subs_array( 'email', lists );
// Write in new data
GRIST.STORAGE.set( 'email', email );
GRIST.READER.add_subscriptions( lists );
// Delete old cookie
jQuery.removeCookie('grist_subscriber', { path: '/' });
} catch(e) {}
}
}; // GRIST.SUBSCRIPTION_TRACKING
/**
* Grist Newsletter Arrival Capturing
*
* Checks for the utm_source=newsletter query var and identifies the reader as
* a subscriber of that newsletter in persistent storage
*
* When a user arrives from an email:
* Assign KM properties
* - subscriber
* - subscriber-[type]
* - subscriber-[specific-type] ex. subscriber-daily
*
* ID user in KM with their email
*
* Store user information in localStorage
* - add to subscriptons array ex. [food]
* - email
*
*/
GRIST.NEWSLETTER_ARRIVAL_CAPTURE = {
init: function(){
// Check if we're coming from a newsletter. No referrers allowed.
if( document.referrer !== '' || GRIST.HELPERS.get_URL_param( 'utm_source' ) !== 'newsletter' )
return;
var sub_data = { sub_type: 'email' },
newsletter_name = this.get_newsletter_name(),
email = GRIST.HELPERS.get_URL_param( 'sub_email' );
if( ! newsletter_name )
return;
sub_data.subs = [newsletter_name];
if( email )
sub_data.sub_email = email;
GRIST.SUBSCRIPTION_TRACKING.mark_as_subscriber( sub_data );
},
/**
* Gets and sanitizes the the newsletter that the user came from
* @return str|bool Newsletter name on success, false if not a valid name
*/
get_newsletter_name: function(){
var newsletter_name = GRIST.HELPERS.get_URL_param( 'utm_campaign' );
var whitelisted_newsletters = [
'daily',
'weekly',
'business-tech',
'climate-energy',
'living',
'food'
];
// The supplied newsletter must be one of our accepted values
if( jQuery.inArray( newsletter_name, whitelisted_newsletters ) === -1 )
return false;
else
return newsletter_name;
}
}; // GRIST.NEWSLETTER_ARIVAL_CAPTURE
/**
* Grist Visit
*
* Sets a cookie to indicate a running visit to the site that will be
* updated on each page view
*
* This cookie will expire after 30 minutes of inactivity
* If you want to know if this particular pageview is the start
* of a new visit, check against GRIST.VISIT.is_new_visit
*/
GRIST.VISIT = {
init: function(){
// Requires that cookies be enabled and jQuery.cookie plugin
if( typeof jQuery.cookie !== 'function' || ! GRIST.HELPERS.are_cookies_enabled )
return;
// Set the params
this.cookie_name = "grist_visit";
this.visit_length = 30; //minutes
this.is_new_visit = false;
// If the visit cookie does not exist, start a new visit
if ( typeof jQuery.cookie( this.cookie_name ) === 'undefined' )
this.start_new_visit();
// Create/refresh the visit cookie
jQuery.cookie(this.cookie_name, '1', { expires: this.calculate_expiration_date(), path:'/' });
},
/**
* @return Date A Date object a certain number of minutes from now
*/
calculate_expiration_date: function(){
var date = new Date();
date.setTime( date.getTime() + ( this.visit_length * 60 * 1000) );
return date;
},
/**
* Initiates a new visit
*/
start_new_visit: function(){
this.is_new_visit = true;
// Increment universal visit counter
GRIST.STORAGE.increment_counter('visits');
// Reset the running log of things that happened during this visit
GRIST.STORAGE.set('visit_events', []);
}
}; // GRIST.VISIT
/**
* Count the number of visits we are getting this month from this person
* and store them in a cookie
*/
GRIST.VISIT.MONTHLY_COUNTER = {
init: function(){
// Requires that cookies be enabled and jQuery.cookie plugin
if( typeof jQuery.cookie !== 'function' || ! GRIST.HELPERS.are_cookies_enabled )
return;
this.cookie_name = 'grist_vtm'; //visits_this_month
// How many visits have we had from this person this month?
this.visits_this_month = parseInt( jQuery.cookie( this.cookie_name ), 10 );
// If we get back NaN it means that the cookie is not set, so this is their first visit this month
if( isNaN( this.visits_this_month ) )
this.visits_this_month = 1;
else // Otherwise, add one to their visit count for this month
this.visits_this_month++;
// Only continue if this is the start of a new visit
if( ! GRIST.VISIT.is_new_visit )
return;
// If this is their second visit this month, fire analytics events
if( this.visits_this_month == 2){
_kmq.push(['record', 'visited-twice-in-month']);
_gaq.push(['_trackEvent', 'Retention', 'visited-twice-in-month', '', 0, true]);
}
// Update visit cookie
jQuery.cookie( this.cookie_name, this.visits_this_month, { expires: this.get_end_of_month(), path:'/' } );
},
/**
* @return Date the last moment of the last day of this month
*/
get_end_of_month: function(){
var d = new Date();
return new Date( d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59 );
}
}; // GRIST.VISIT.MONTHLY_COUNTER
/**
* Fires an event once for each type of page viewed during visit
*/
GRIST.VISIT.PAGEVIEWS = {
init: function(){
// Requires that we have pageview data
if( typeof GRIST.PAGEVIEW === 'undefined' || ! GRIST.PAGEVIEW.hasOwnProperty( 'type' ) )
return;
// Requires that cookies be enabled and jQuery.cookie plugin
if( typeof jQuery.cookie !== 'function' || ! GRIST.HELPERS.are_cookies_enabled )
return;
// Requires local storage
if( ! GRIST.STORAGE.is_enabled )
return;
var visit_events = GRIST.STORAGE.get('visit_events');
// If there is no local storage key 'visit_events', bail
if( jQuery.type( visit_events ) !== 'array' )
return;
var visit_event = 'visited-' + GRIST.PAGEVIEW.type;
// If we've recorded an event of this type during this visit, bail
if( jQuery.inArray( visit_event, visit_events ) !== -1 )
return;
// Add it to visit event log so we don't record a second viewing
GRIST.STORAGE.add_to_array( 'visit_events', [ visit_event ] );
// Record visit event
_kmq.push(['record', visit_event ]);
} // init
}; // GRIST.VISIT.PAGEVIEWS
/**
* Looks for a cookie from services.grist.org and records
* a donation for this user in localstorage
*/
GRIST.DONATION_TRACKING = {
init: function(){
//Get the cookie
jQuery.cookie.json = true;
var donation_data = jQuery.cookie( 'grist_donor' );
if( ! donation_data || typeof donation_data !== 'object' )
return;
// Legacy support
if( donation_data.hasOwnProperty('t') )
donation_data = { time: donation_data.t * 1000 };
// Record the information in local storage
GRIST.STORAGE.add_to_array( 'donations', [donation_data] );
// Delete the cookie
jQuery.removeCookie( 'grist_donor', { path: '/', domain: 'grist.org' } );
}
}; // GRIST.DONATION_TRACKING
/**
* Records an event when readers reach the end of an article
*/
GRIST.DEPTH_TRACKING = {
init: function(){
// Requires that we have pageview data
if( typeof GRIST.PAGEVIEW === 'undefined' || ! GRIST.PAGEVIEW.hasOwnProperty( 'type' ) )
return;
var depth_event_recorded = false,
story_type = GRIST.PAGEVIEW.type,
$end_of_article = jQuery('#grist-end-of-article');
// If we're not on an article page, bail
if( $end_of_article.length < 1 )
return;
// Look for the end of article tag on each scroll
// scroll event polling @see http://ejohn.org/blog/learning-from-twitter/
var didScroll = false;
jQuery(window).on('scroll.grist_depth_tracking', function(){
didScroll = true;
});
var depth_scroll_interval = setInterval(function() {
if( depth_event_recorded )
return;
if ( didScroll ) {
didScroll = false;
if( GRIST.HELPERS.is_el_on_screen( $end_of_article ) ){
// Set flags, clear interval, and remove event handler to free up resources
depth_event_recorded = true;
clearInterval( depth_scroll_interval );
jQuery(window).off('scroll.grist_depth_tracking');
// Fire analytics events
_kmq.push(['record', 'reached-end-of-article']);
_kmq.push(['record', 'reached-end-of-article-' + story_type]);
_gaq.push(['_trackEvent', 'Retention', 'reached-end-of-article', story_type, 0, true]);
// We are particularly interested in deeper green articles,
// so fire a special event if one of those is finished
var deep_green_types = ['story', 'news', 'people', 'basics'];
if( jQuery.inArray( story_type, deep_green_types ) === -1 )
return;
_kmq.push(['record', 'reached-end-of-article-deep-green']);
_gaq.push(['_trackEvent', 'Retention', 'reached-end-of-article', 'deep-green', 0, true]);
}
}
}, 250);
}
};
/**
* Tracks usage of our discovery mechanisms
*/
GRIST.DISCOVERY_TRACKING = {
init: function(){
// Listen for people clicking on our discovery sources
this.listen_for_clicks();
//Listen for people landing on our pages from another grist page
this.listen_for_landing();
},
/**
* Listens for clicks on discovery sources like outbrain and skyboxes
* and fires analytics events when they occur
*/
listen_for_clicks: function(){
var action = 'discovery-click',
_this = this;
// A list of discovery sources. Those that are injected dynamically need
// to have an injection_scope so we can listen for clicks after their injection
var discovery_sources = [
{ label: 'story-infinite-scroll', container: '.infinite-scroll-item', link_selector: 'a', injection_scope: '.infinite-stories' },
{ label: 'responsive-skybox-large', container: '#skyboxes', link_selector: 'a', injection_scope: '#main-nav' },
{ label: 'outbrain', container: '.ob_dual_left', link_selector: 'a', injection_scope: '#recommendations' },
{ label: 'responsive-skybox-small', container: '.small-menu-bar .recommended-stories', link_selector: 'a' },
{ label: 'special-features-widget', container: '#zone-homepage-special-features', link_selector: 'a' },
{ label: 'desktop-skybox', container: '#masthead ', link_selector: '.skybox' },
{ label: 'umbra-widget', container: '.umbra-widget', link_selector: 'a' },
{ label: 'most-viewed-widget', container: '#widget-most-viewed ', link_selector: 'a' }
];
jQuery.each( discovery_sources, function(i, discovery_source){
// A callback function for our discovery listeners
function discovery_click_callback(e){
_this.record_event(action, discovery_source.label);
}
if( discovery_source.hasOwnProperty('injection_scope') ){
jQuery(discovery_source.injection_scope).on('click', discovery_source.container + ' ' + discovery_source.link_selector, discovery_click_callback);
} else {
jQuery(discovery_source.container).find(discovery_source.link_selector).on('click', discovery_click_callback);
}
});
},
/**
* Listens for people landing on a grist page from another grist page
*/
listen_for_landing: function(){
var referrer = document.referrer;
// If this is a direct visit, bail
if( ! referrer)
return;
// If this is a refresh, bail
if( referrer === document.URL )
return;
// Get the domain name of the referrer
var referral_components = referrer.split('/');
if( referral_components.length < 3 )
return;
var referral_domain = referral_components[2];
// Check that it contains one of our accepted domain names
var is_from_grist = (referral_domain.indexOf('grist.org') !== -1);
var is_from_local_outbrain = (referral_domain.indexOf('traffic.outbrain.com') !== -1);
// If this isn't from an internal source, bail
if( !is_from_grist && !is_from_local_outbrain )
return;
_gaq.push(['_trackEvent', 'Retention', 'discovery-landing', referrer, 0, true]);
},
/**
* Records an analytics event
* @param {str} action Action name i.e. 'discovery-click'
* @param {str} type Action type i.e. 'skybox'
*/
record_event: function( action, type ){
if( typeof action === 'undefined' || typeof type === 'undefined')
return;
_kmq.push(['record', action]);
_kmq.push(['record', action + '-' + type]);
_gaq.push(['_trackEvent', 'Retention', action, type, 0, true]);
}
}; // GRIST.DISCOVERY_TRACKING
/**
* Utility functions
*/
GRIST.HELPERS = {
/**
* Are cookies enabled?
* @see - http://sveinbjorn.org/cookiecheck
* @return bool
*/
are_cookies_enabled: (function(){
var cookieEnabled = (navigator.cookieEnabled) ? true : false;
if (typeof navigator.cookieEnabled == "undefined" && !cookieEnabled) {
document.cookie="testcookie";
cookieEnabled = (document.cookie.indexOf("testcookie") != -1) ? true : false;
}
return (cookieEnabled);
})(),
/**
* Gets a parameter from the url
* @see - http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
* @param str param name of the param to get
* @return str|bool value of the parameter if set, false if not set
*/
get_URL_param: function( param ) {
return (RegExp(param + '=' + '(.+?)(&|$)').exec(location.search) || [false, false])[1];
},
/**
* Checks to see if an element is on the screen
*
* @see http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling
*
* @param jQuery DOM Element el
* @return bool
*/
is_el_on_screen: function( $el ){
if( typeof $el === 'undefined' || $el.length === 0 )
return false;
var docViewTop = jQuery(window).scrollTop();
var docViewBottom = docViewTop + jQuery(window).height();
var elemTop = $el.offset().top;
var elemBottom = elemTop + $el.height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
},
/**
* Returns the canonical page url for the current page, if set in a tag
* @return string the url of the canonical version of the page
*/
get_canonical_link: function() {
return jQuery('link[rel=canonical]').attr('href');
}
}; // GRIST.HELPERS;
/*
* addthis.js
*
*
* Initiates a social tracker, which fires Google Analytics and
* KISSMetrics events when people like or follow Grist or share a story.
*
* @requires 'grist-universal-js'
*/
var GRIST = GRIST || {};
if( typeof( addthis ) === 'object' && typeof( addthis.addEventListener ) === 'function' ) {
// Start listening for likes and shares
addthis.addEventListener('addthis.menu.share', grist_social_tracker);
}
/**
* Records GA/KISSmetrics events for shares/subscriptions
*
* @param addthis_evt The addthis event (a like or a share)
*/
function grist_social_tracker( addthis_evt ){
// check which service the user shared with
var event_service = addthis_evt.data.service;
// don't record print or email events
if( event_service === 'print' || event_service === 'email' )
return;
var event_name = 'unknown type'; // subscribe or share event
var event_atts;
var subscription_type = grist_get_addthis_subscription_type( addthis_evt );
if( subscription_type ){
// if this is a subscription, announce it
GRIST.SUBSCRIPTION_TRACKING.announce_subscribe({
sub_type: subscription_type,
subs: [subscription_type]
});
} else {
// if this is a share, record the type (tweet, facebook, googleplus, etc) and the url shared
event_name = 'share';
event_atts = { 'share_type': event_service };
_kmq.push(['record', event_name, event_atts]);
_gaq.push(['_trackEvent', event_name, event_service, document.URL, 0, true]);
}
} // end grist_km_traffic_quality_tracker()
/**
* Check what type of subscription just occured
*
* @param addthis_evt The addthis event
* @return string "facebook" if the user liked Grist.org or "twitter" if they followed Grist.org on twitter
* @return boolean false if it is a different kind of AddThis event (such as a page specific like)
*
*/
function grist_get_addthis_subscription_type( addthis_evt ){
var share_type = addthis_evt.data.service;
var share_url = addthis_evt.data.url;
if( share_type === "facebook_like" && ( share_url === "https://www.facebook.com/grist.org" || share_url === 'http://grist.org/' ) ){
return 'facebook';
} else if( share_type === "twitter_follow_native" && share_url === "http://grist.org/" ){
return 'twitter';
} else {
return false;
}
};